package com.demiroot.server;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import com.demiroot.server.HeaderData.HeaderField;
import com.demiroot.server.pages.Page;


public class Server {
	private ServerSocket servSock;
	public static String ENDL = "" + (char)13 + (char)10;
	
	public static boolean PRINT_INCOMING = true;
	public static boolean PRINT_HEADERS = true;
	
	private Map<String, ServerSession> sessions = new HashMap<String, ServerSession>(); 
	private Timer sessionDeamon;
	private static Config config;
	
	private List<Class> pages = new LinkedList<Class>();
	private Map<Class, Method> isThisYouMethods = new HashMap<Class, Method>();
	
	private UserManager userManager;
	
	public Server() throws Exception {
		userManager = new UserManager();
		ServerUtils.load();
		loadConfig();
		
		for (Config.Folder f : config.getJdocsLocs()) {
			loadJavaPages(f.getLocation());
		}
		
		//sets a simple timer to clear unsed sessions
		sessionDeamon = new Timer(true);
		TimerTask tt = new TimerTask(){
			@Override
			public void run() {
				for (String id: sessions.keySet()) {
					ServerSession curr= sessions.get(id);
					if (curr.hasExpired()) {
						sessions.remove(id);
					}
				}
			}
		};
		sessionDeamon.scheduleAtFixedRate(tt, 0, 1000*60*10);
	}
	
	public void start(int port) throws Exception {
		servSock = new ServerSocket(port);
		while (true) {
			new Thread(new Request(servSock.accept(),this)).start();
		}
	}
	
	public void loadConfig() throws Exception {
		SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
		XMLReader reader = parser.getXMLReader();
		config = new Config();
		reader.setContentHandler(new ServerConfigReader(config));
		InputStream is = ClassLoader.getSystemResource("config.xml").openStream();
		reader.parse(new InputSource(is));
	}

	public boolean sessionExists(String sessionID) {
		return sessions.containsKey(sessionID);
	}
	
	public void addSession(ServerSession ss) {
		sessions.put(ss.getSessionID(), ss);
	}
	
	public ServerSession getSession(String id) {
		return sessions.get(id);
	}

	public UserManager getUserManager() {
		return userManager;
	}
	
	public Config getConfig() {
		return config;
	}
	
	public Method getIsThisYouMethod(Class page) {
		if (isThisYouMethods.containsKey(page)) {
			return isThisYouMethods.get(page);
		} else {
			try {				
				Method isThisYou = page.getMethod("isThisYou", String.class);
				if (Modifier.isStatic(isThisYou.getModifiers()) && 
						Modifier.isPublic(isThisYou.getModifiers()) &&
						isThisYou.getReturnType().equals(boolean.class)) {
					isThisYouMethods.put(page, isThisYou);
					return isThisYou;
				}
			} catch (Exception e) {
			}
			Logger.log("error", "Class " + page + " does not have a\npublic static boolean" +
					" isThisYou(String) method, and can not be servered!");
			isThisYouMethods.put(page, null);
			return null;
		}
	}
	
	private HTTPResponse getPage(OutputStream os, HeaderData header, byte[] content, Server server) throws Exception {
	
		Map<String,String> vars = null;
		if (header.get(HeaderField.CONTENT_TYPE) != null 
				&&header.get(HeaderField.CONTENT_TYPE ).contains("x-www-form-urlencoded")) {
			vars = ServerUtils.seperateContent(content);
			Logger.log("url-encoded-vars",vars);
		} else if (header.getInt(HeaderField.CONTENT_LENGTH) != 0) {
			
		}
		
		for (Class p : getJavaPages()) {
			Method isThisYou = getIsThisYouMethod(p);
			if (isThisYou != null) {
				Boolean b = (Boolean)isThisYou.invoke(p,header.getURI());
				if (b.equals(Boolean.TRUE)) {
					Page page = (Page)p.newInstance();
					return page.getPage(new HTTPRequestImpl(content,header,server,vars));
				}
			}
		}
		
		URL fileURL = ClassLoader.getSystemResource("Server\\htdocs\\" + header.getURI());
		if (fileURL  == null || !(new File(fileURL.toURI()).exists()) 
				|| !(new File(fileURL.toURI()).canRead())) {
			return new FileNotFoundResponse(header.getURI(),config);
		}
		
		return new PhysicalItemResponse(fileURL);
	}
	
	public List<Class> getJavaPages() {
		return pages;
	}
	
	public void loadJavaPages(String folderPath) throws URISyntaxException {
		URL loc = ClassLoader.getSystemResource(folderPath);
		File folder = new File(loc.toURI());
		
		File[] files = folder.listFiles(new FilenameFilter(){
			public boolean accept(File dir, String name) {
				return name.endsWith(".class");
			}
		});
		
		for (File f : files)
		{
			Logger.log("javaPage", "Checking page: " + f);
			try {
				Class<?> page = Class.forName(folderPath + "." + f.getName().replace(".class", ""));
				if (isPage(page)) {
					Logger.log("javaPage", "Added page: " + page);
					pages.add(page);
				}
			}catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	private static boolean isPage(Class<?> page) {
		return !Modifier.isAbstract(page.getModifiers()) 
			&& implementsPage(page); 
	}
	
	private static boolean implementsPage(Class<?> page) {
		if (page == null) return false;
		if (page.getClass().equals(Object.class)) {
			return false;
		}
		for (Class<?> c : page.getInterfaces()) {
			if (c.equals(Page.class)) {
				return true;
			}
		}
		return implementsPage(page.getSuperclass());
	}

	public boolean isProxyEnabled() {
		return true;
	}
}
